前兩天講了 server component 與 client component,Next官方文章也有列出一些使用 component pattern,今天就來看看有哪些需要注意的地方。
在 client component 當中,我們可以使用 props、context 或是其他第三方的狀態管理來共享資料。但在 server component 只能使用 props 一層一層傳,如果需要傳遞的結構很深,就會有 props drilling 的問題。
我們可以在每一個需要資料的 server component 都使用內建的 fetch
或是使用被 React cache
包裝過的 function 取得資料,這樣就不需要將結果傳遞給其他 component 了,因為每個 component 都有 fetch 資料。
你可能會想問,這樣不就需要打很多次一模一樣的 api?如果有三個 component 都要 user api 資料,不就要 fetch 三次?
這就要歸功於 Next 的 Request Memoization 功能,使用 fetch 或是 cache
包裝過的 function 取得資料,如果傳進來的參數都一樣,那 Next 就會自動把 request 合併,所有 request 都會取得同一個結果。
昨天的文章有提到要避免 server component 傳入 client component,事實上不只 component,只要是你希望它運行在 server 上的所有東西,都要避免傳入 client component。
可以看看底下這段 code 有什麼需要注意的地方
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}
答案是 authorization: process.env.API_KEY
這段。因為這段環境變數沒有要共享給 client 端使用,如果要開放給 client 端使用,則需要加上 NEXT_PUBLIC
當作前綴。
雖然 fetch 可以在 server 及 client 使用,但會有一些情境是只想在某一端使用的。
這時候就可以使用 server-only
這個套件,將這個套件 import 進只想運行在 server 的檔案。它會在 build time 時檢查是否有 server-only
的檔案被 import 進了 client component,如果有個話就會直接噴錯。
npm install server-only
import 'server-only'
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}
當然同樣也會有只想執行在 client 的檔案,像是有操作 dom 或 browser API,這時也可以像上面的 server-only 的做法一樣,只是換成 import 'client-only'
。
昨天的文章有提到如何轉換第三方套件,可以使用 use client
重新 export 套件,讓 Next 知道該套件是 client component,避免引入至 server component 時報錯。
使用 client component 很有可能會使用到 React 的 context api 傳遞資料,最有可能引入 context api 的地方就是 layout.tsx
,因為這是一個共享的 UI 佈局,layout.tsx 不會隨著路由切換消失。
當我們在 layout.tsx
使用 createContext
會報錯,因為 layout.tsx
是一個 server component。雖然可以加上 'use client'
讓它正常運行,但我們最好保持 layout.tsx
是 server component,理由在下方會談到的「盡可能把 client component 推到末端」這點有關
import { createContext } from 'react'
// createContext 不支援使用在 server component
export const ThemeContext = createContext({})
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
</body>
</html>
)
}
所以這時可以把 providers 全部抽出來當成一個 client component
,再把這個 component 引入到 layout.tsx
。
// 把所有 provider 都丟來這就對了
'use client'
import { createContext } from 'react'
import { ThirdPartyProvider } from 'third-party'
export const ThemeContext = createContext({})
export default function Providers({ children }) {
return (
<ThemeContext.Provider value="dark">
<ThirdPartyProvider>
{children}
<ThirdPartyProvider>
</ThemeContext.Provider>
)
}
import Providers from './providers'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
昨天的鐵人賽 有講到,client component 因為要傳到 client 端運行,是會有 js bundle 的,所以我們要盡可能地把 client 的邊界往末端去推。
什麼意思呢?我們先來看渲染出來的樹長怎樣
以這張圖來看,下方就代表末端,Next 建議我們把 client component 儘可能地往下擺放。
實作上就是盡量把 server component 就可以渲染的 UI 從 client component 搬出去。
以上方講到的 layout.tsx 這支範例來看,我們會希望 providers.tsx 盡量減少 server component 就可以渲染的 UI,只包含一定得在 client 才能運行的 code。
import Providers from './providers'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
import Providers from './providers'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<Providers>
<html>
<body>
{children}
</body>
</html>
</Providers>
)
}